Skip to content

feat(validators): per-resource SHACL validation mode#80

Merged
jdsika merged 3 commits into
mainfrom
feat/per-resource-validation
Jun 12, 2026
Merged

feat(validators): per-resource SHACL validation mode#80
jdsika merged 3 commits into
mainfrom
feat/per-resource-validation

Conversation

@jdsika

@jdsika jdsika commented Jun 10, 2026

Copy link
Copy Markdown

feat(validators): per-resource SHACL validation mode

Why

make validate shacl (and the downstream consumers, e.g. harbour-credentials
and simpulse-id-credentials) currently merge every --data-paths input into a
single RDF graph before validation. That is wrong for Verifiable Credentials and
DID documents, which legitimately reuse the same IRIs across files:

  • a credential's credentialSubject.id is a DID that is also the subject of a
    DID document (carrying verificationMethod/service);
  • the same credential is embedded in several presentations.

When these collapse onto one node, closed shapes reject the "foreign"
properties (ClosedConstraint), shared blank-node values multiply (maxCount),
and — more dangerously — missing required properties get silently borrowed
from another file in the merge, hiding real defects.

This matches SHACL's documented single-graph limitation and the W3C VC WG's
2024-01-31 decision
to keep SHACL out of core VC validation for exactly this reason.

What

A new opt-in --per-resource mode that validates each document in its own
graph
, while sharing the expensive, data-independent work:

Addition Purpose
ShaclValidator.validate_each() load shapes + pre-close ontology once, validate each file isolated
ShaclValidator._abox_inference() fast targeted RDFS (rdfs2/3/7/9) over the small data graph via indexed TBox lookups
_load_data(load_fixtures=False) don't merge referenced DID docs onto credential subjects
validation_suite --per-resource CLI flag; validates former "fixture" files as their own resources too

Performance

Naive per-file validation is ~58s/file (parse 19s + RDFS inference 29s + pyshacl
10s, all re-done per file). By loading shapes and pre-computing the ontology
closure once, validate_each() brings this to ~3s/file (~20x), so a 17-file
sweep runs in ~40s instead of ~16 min.

Backwards compatibility

Everything is gated behind --per-resource (default off). The merged default
path is untouched.

  • OMB test suite: 469 passed, 1 xfailed, 1 failed.
  • The single failure (test_backslash_traversal_rejected) is pre-existing and
    unrelated
    — a Linux path-separator quirk, reproducible with these changes
    stashed; it lives in test_http_artifact_resolver.py, which this PR does not
    touch.

Negative test

Confirmed validate_each still catches violations: a credential missing required
evidence → FAIL (minCount); an unknown extra property → FAIL (closed-shape).

Add an opt-in `--per-resource` mode that validates each JSON-LD document
in its own graph instead of merging all `--data-paths` inputs into one
graph.

Merging multiple linked documents that legitimately reuse IRIs — e.g. a
Verifiable Credential whose subject DID is also described by a DID
document, or a credential embedded across several files — collapses those
distinct resources onto a single RDF node. Under closed shapes this
produces spurious ClosedConstraint and maxCount violations, and it can
silently mask missing required properties by borrowing them across files.
Per-resource validation is the correct grain for VC/DID data (cf. SHACL's
single-graph model and the W3C VCWG 2024-01-31 discussion of it).

- `ShaclValidator.validate_each()`: load the union of required shapes and
  compute the ontology RDFS closure once, then validate each file's data
  graph in isolation. ~20x faster than calling `validate()` per file
  (which re-parses shapes and re-infers the ontology every time).
- `ShaclValidator._abox_inference()`: fast targeted RDFS (rdfs2/3/7/9)
  over the small data graph using indexed TBox lookups against the
  pre-closed ontology, avoiding a full-graph re-inference per resource.
- `_load_data(load_fixtures=...)`: per-resource mode skips fixture
  resolution so referenced DID documents are not folded onto credential
  subjects.
- `validation_suite`: `--per-resource` flag; in this mode the former
  "fixture" files are validated as their own resources rather than only
  used for IRI resolution.

All additions are gated behind `--per-resource` (default off); the merged
default path is unchanged. Verified against the existing suite: 469 passed
(the one pre-existing `test_backslash_traversal_rejected` failure is a
Linux path-separator quirk, unrelated to this change).

Signed-off-by: jdsika <carlo.van-driesten@vdl.digital>
@jdsika

jdsika commented Jun 10, 2026

Copy link
Copy Markdown
Author

Consumed by reachhaven/harbour-credentials#13, which pins the submodule to this branch and switches make validate shacl + the story pipeline to --per-resource.

jdsika and others added 2 commits June 10, 2026 21:50
The linkml fork branch feat/envited-x-pipeline was rebased onto the synced
upstream main and force-pushed, orphaning the previous pin 3d3a52a3. Point
the submodule at the rebased head b2b3dba5d2 — the same ENVITED-X feature
commits (deterministic/normalize-prefixes/closed/covering-axiom-warning,
etc.) now cleanly on top of current upstream main.

Signed-off-by: jdsika <carlo.van-driesten@vdl.digital>
Regenerate openlabel-v2.shacl.ttl to match the rebased
feat/envited-x-pipeline linkml fork, which includes:

- deterministic blank node ordering via WL hashing
- str(?flag) != "true" SPARQL comparison (xsd:boolean/string safe)

Signed-off-by: Carlo van Driesten <carlo.van-driesten@bmw.de>
@jdsika
jdsika merged commit d452ec2 into main Jun 12, 2026
12 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant